package org.jsserv;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.jsserv.resource.ScriptHolder;
import org.jsserv.resource.URLResourceLoader;
import org.jsserv.util.WebAppPathUtil;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.NativeFunction;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.springframework.beans.factory.annotation.Required;

/**
 * Holds the current state of the client's HTML Document Object Model, updates
 * it with click events and sends it back to the client. DOMState objects are
 * stored in the Spring session and defined in applicationContext.xml
 * 
 * @author shelmberger
 */
public class DOMState
{
    private static final long serialVersionUID = -6510583664404637204L;

    protected static Logger log = Logger.getLogger(DOMState.class);

    static class MyFactory extends ContextFactory
    {
        protected boolean hasFeature(Context cx, int featureIndex)
        {
            if (featureIndex == Context.FEATURE_DYNAMIC_SCOPE)
            {
                return useDynamicScope;
            }
            return super.hasFeature(cx, featureIndex);
        }
    }

    static
    {
        ContextFactory.initGlobal(new MyFactory());

        Context cx = Context.enter();
        ScriptableObject sharedScope = null;
        try
        {
            // Initialize the standard objects (Object, Function, etc.)
            // This must be done before scripts can be executed. The call
            // returns a new scope that we will share.
            sharedScope = cx.initStandardObjects(null, true);
        }
        catch (Exception e)
        {
            log.error("error in static block", e);
        }
        finally
        {
            Context.exit();
        }
        SHARED_SCOPE = sharedScope;
    }

    private Map<URL, ScriptHolder> patches = new HashMap< URL, ScriptHolder>();

    public final static Scriptable SHARED_SCOPE;

    static boolean useDynamicScope = true;

    private List<ScriptHolder> environmentScripts;

    private Scriptable scope;

    private String doctype;

    private List<ScriptHolder> pageScripts;

    private String baseURI;
    
    private URLResourceLoader<ScriptHolder> loader;

    private String contextPath;

    public String getContextPath()
    {
        return contextPath;
    }
    
    @Required
    public void setLoader(URLResourceLoader<ScriptHolder> loader)
    {
        this.loader = loader;
    }
    
    private List<ScriptHolder> scriptList(List<String> paths)
    {
        List<URL> urls = pathListToURLList(paths);
        List<ScriptHolder> scripts = new ArrayList<ScriptHolder>();
        for (URL url : urls )
        {
            try
            {
                ScriptHolder script=loader.load(url, true);
                scripts.add(script);
            }
            catch (Exception e)
            {
                log.error(e);
            }
        }
        return scripts;
    }
    
    private URL pathToURL(String path)
    {
        return WebAppPathUtil.getURL(path);
    }

    private List<URL> pathListToURLList(List<String> paths)
    {
        List<URL> urls=new ArrayList<URL>(paths.size());
        for (String path : paths)
        {
            urls.add(pathToURL(path));
        }
        return urls;
    }

    public void setEnvironmentScripts(List<String> environmentScripts)
    {
        
        this.environmentScripts = scriptList(environmentScripts);
    }

    public void setPageScripts(List<String> pageScripts)
    {
        this.pageScripts = scriptList(pageScripts);
    }

    public void setPatches(Map<String, String> extensions)
    {
        try
        {
            for (Map.Entry<String, String> entry : extensions.entrySet())
            {
                URL target = pathToURL(entry.getKey());
                URL patch = pathToURL(entry.getValue());
                
                ScriptHolder holder=loader.load(patch, true);
                this.patches.put(target, holder);
            }
        }
        catch(Exception e)
        {
            log.error("",e);
        }
    }

    public DOMState()
    {
        Context cx = Context.enter();
        try
        {
            scope = DOMState.createScope(cx);
        }
        catch (Exception e)
        {
            log.error("", e);
        }
        finally
        {
            Context.exit();
        }
    }

    public static Scriptable createScope(Context cx)
    {
        Scriptable scope = cx.newObject(SHARED_SCOPE);
        scope.setPrototype(SHARED_SCOPE);

        scope.setParentScope(null);
        return scope;
    }

    private void runScripts(Context cx, Scriptable scope, List<ScriptHolder> scripts)
    {
        for (ScriptHolder script : scripts)
        {
            try
            {
                if (log.isDebugEnabled())
                {
                    log.debug("execute " + script);
                }
                script.getContent().exec(cx, scope);
            }
            catch (IOException e)
            {
                log.error(e);
            }
        }
    }

    /**
     * Initializes the {@link DOMState} state with the given HTML document.
     * 
     * @param html
     */
    public void createDocument(String baseURI, String contextPath, String html)
    {
        if (log.isDebugEnabled())
        {
            log.debug("enter createDocument()");
        }

        this.baseURI=baseURI;
        this.contextPath=contextPath;

        int pos = html.indexOf("<!DOCTYPE");
        if (pos >= 0)
        {
            int end = html.indexOf('>', pos);
            doctype = html.substring(pos, end + 1);
            html = html.substring(end + 1);
        }

        Context cx = Context.enter();
        try
        {
            runScripts(cx, scope, environmentScripts);

            String script = "window.createDocument(" + html.trim() + ");";
            cx.evaluateString(scope, script, "createDocumentCall", 1, null);
            scope.put("domState", scope, Context.javaToJS(this,scope));
            runScripts(cx, scope, pageScripts);
            
            cx.evaluateString(scope, "window.triggerOnload();", "invokeOnload", 1, null);
        }
        finally
        {
            Context.exit();
        }
    }

    /**
     * Signals the given event to the element that was registered with the given
     * uuid.
     * 
     * @param uuid
     * @param event
     */
    public void signalEvent(String uuid, String event)
    {
        if (uuid == null)
            throw new NullPointerException("uuid is null");
        if (event == null)
            throw new NullPointerException("event is null");

        Context cx = Context.enter();
        try
        {
            ScriptableObject window=(ScriptableObject)scope.get("window", scope);
            NativeFunction triggerEvent=(NativeFunction)window.get("triggerEvent", window);
            
            // window.triggerEvent( uuid, event);
            triggerEvent.call(cx, scope, window, new Object[] { uuid, event} );
        }
        finally
        {
            Context.exit();
        }
    }

    /**
     * Dumps the current DOM as String.
     * 
     * @return html document
     */
    public String dumpDOM()
    {
        // We need a new Context for this thread.
        Context cx = Context.enter();
        try
        {
            String out = (String)cx.evaluateString(scope, "document.innerHTML", "dumpDOM", 1, null);
            return doctype + out;
        }
        finally
        {
            Context.exit();
        }
    }
    
    /**
     * Loads and executes a script in the current script context. if a patch script
     * is defined it will be executed after the target script.
     * @param url
     */
    public void loadScript(String url) 
    {
        Context cx = Context.enter();
        try
        {
            URL resolved;
            if ( url.startsWith("http://") || url.startsWith("https://") || url.startsWith("ftp://") )
            {
                try
                {
                    resolved=new URL(url);
                }
                catch (MalformedURLException e)
                {
                    log.info("Invalid url referenced in document",e);
                    return;
                }
            }
            else
            {
                if (url.startsWith(contextPath))
                {
                    url = url.substring(contextPath.length());
                }

                if (!url.startsWith("/"))
                {
                    int pos=baseURI.lastIndexOf('/');
                    url=baseURI.substring(0, pos+1)+url;
                    resolved = pathToURL(url);
                }
                resolved=pathToURL(url);
            }
                 
            
            ScriptHolder patch = patches.get(resolved);
            log.debug("load script " + resolved + " (with patch = " + patch + ")");
            
            try
            {
                ScriptHolder target=loader.load(resolved, true);
                target.getContent().exec(cx, scope);
                if (patch != null)
                {
                    patch.getContent().exec(cx, scope);                    
                }
            }
            catch(Exception e)
            {
                log.info("",e);
            }
        }
        finally
        {
            Context.exit();
        }
    }
}
